/**
 * A queue
 * @param {Object} options
 *            Available options:
 *            - delay: number    When provided, the queue will be flushed
 *                               automatically after an inactivity of this delay
 *                               in milliseconds.
 *                               Default value is null.
 *            - max: number      When the queue exceeds the given maximum number
 *                               of entries, the queue is flushed automatically.
 *                               Default value of max is Infinity.
 * @constructor Queue
 */
class Queue {
  constructor(options) {
    // options
    this.delay = null;
    this.max = Infinity;

    // properties
    this._queue = [];
    this._timeout = null;
    this._extended = null;

    this.setOptions(options);
  }

  /**
   * Update the configuration of the queue
   * @param {Object} options
   *            Available options:
   *            - delay: number    When provided, the queue will be flushed
   *                               automatically after an inactivity of this delay
   *                               in milliseconds.
   *                               Default value is null.
   *            - max: number      When the queue exceeds the given maximum number
   *                               of entries, the queue is flushed automatically.
   *                               Default value of max is Infinity.
   */
  setOptions(options) {
    if (options && typeof options.delay !== 'undefined') {
      this.delay = options.delay;
    }
    if (options && typeof options.max !== 'undefined') {
      this.max = options.max;
    }

    this._flushIfNeeded();
  }

  /**
   * Extend an object with queuing functionality.
   * The object will be extended with a function flush, and the methods provided
   * in options.replace will be replaced with queued ones.
   * @param {Object} object
   * @param {Object} options
   *            Available options:
   *            - replace: Array.<string>
   *                               A list with method names of the methods
   *                               on the object to be replaced with queued ones.
   *            - delay: number    When provided, the queue will be flushed
   *                               automatically after an inactivity of this delay
   *                               in milliseconds.
   *                               Default value is null.
   *            - max: number      When the queue exceeds the given maximum number
   *                               of entries, the queue is flushed automatically.
   *                               Default value of max is Infinity.
   * @return {Queue} Returns the created queue
   */
  static extend(object, options) {
    const queue = new Queue(options);

    if (object.flush !== undefined) {
      throw new Error('Target object already has a property flush');
    }
    object.flush = () => {
      queue.flush();
    };

    const methods = [{
      name: 'flush',
      original: undefined
    }];

    if (options && options.replace) {
      for (const name of options.replace) {
        methods.push({
          name,
          original: object[name]
        });
        queue.replace(object, name);
      }
    }

    queue._extended = {
      object,
      methods
    };

    return queue;
  }

  /**
   * Destroy the queue. The queue will first flush all queued actions, and in
   * case it has extended an object, will restore the original object.
   */
  destroy() {
    this.flush();

    if (this._extended) {
      const object = this._extended.object;
      const methods = this._extended.methods;

      for (const method of methods) {
        if (method.original) {
          object[method.name] = method.original;
        }
        else {
          delete object[method.name];
        }
      }

      this._extended = null;
    }
  }

  /**
   * Replace a method on an object with a queued version
   * @param {Object} object   Object having the method
   * @param {string} method   The method name
   */
  replace(object, method) {
    const me = this;
    const original = object[method];
    if (!original) {
      throw new Error(`Method ${method} undefined`);
    }

    object[method] = function () {
      // create an Array with the arguments
      const args = [];
      for (let i = 0; i < arguments.length; i++) {
        args[i] = arguments[i];
      }

      // add this call to the queue
      me.queue({
        args,
        fn: original,
        context: this
      });
    };
  }

  /**
   * Queue a call
   * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry
   */
  queue(entry) {
    if (typeof entry === 'function') {
      this._queue.push({fn: entry});
    }
    else {
      this._queue.push(entry);
    }

    this._flushIfNeeded();
  }

  /**
   * Check whether the queue needs to be flushed
   * @private
   */
  _flushIfNeeded() {
    // flush when the maximum is exceeded.
    if (this._queue.length > this.max) {
      this.flush();
    }

    // flush after a period of inactivity when a delay is configured
    clearTimeout(this._timeout);
    if (this.queue.length > 0 && typeof this.delay === 'number') {
      const me = this;
      this._timeout = setTimeout(() => {
        me.flush();
      }, this.delay);
    }
  }

  /**
   * Flush all queued calls
   */
  flush() {
    while (this._queue.length > 0) {
      const entry = this._queue.shift();
      entry.fn.apply(entry.context || entry.fn, entry.args || []);
    }
  }
}

export default Queue;
